﻿/*
   Copyright 2012 University of Southampton

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Xml;
using System.Data.SqlClient;
using System.Data.EntityClient;
using uk.ac.soton.ses.DAL;
using System.Data;
using System.Diagnostics;

namespace uk.ac.soton.ses
{
    /// <summary>
    /// XML serialisable configuration class containing the HDC configuration data.
    /// Database connection settings are stored in the XML file, while the remaining settings are stored in the HDC as containers with a special type.
    /// </summary>
    [XmlRoot("hdcconfiguration")]
    public class HDCConfigurationOptions
    {
        //Fields
        private HDCConfigurationOptionsDatabase _databaseOptions = null;
        private HDCConfigurationOptionsFileSystemWatcher _fileSystemWatcherOptions = null;
        private HDCConfigurationOptionsSharePoint _sharePointOptions = null;

        //Properties

        /// <summary>
        /// The configuration options for accessing the database. These are loaded from the file system.
        /// </summary>
        [XmlElement("database")]
        public HDCConfigurationOptionsDatabase DatabaseOptions { get { return _databaseOptions; } set { _databaseOptions = value; } }

        /// <summary>
        /// The configuration options for the file system watcher. Most of these are loaded from the database, with the database record ID being read from the configuration file in the file system
        /// </summary>
        [XmlElement("filesystemmonitor")]
        public HDCConfigurationOptionsFileSystemWatcher FileSystemWatcherOptions { get { return _fileSystemWatcherOptions; } set { _fileSystemWatcherOptions = value; } }

        /// <summary>
        /// The configuration options for SharePoint (mainly where its root site collection web page is). Loaded from the database, with the database record ID being read from the configuration file in the file system
        /// </summary>
        [XmlElement("sharepoint")]
        public HDCConfigurationOptionsSharePoint SharePointOptions { get { return _sharePointOptions; } set { _sharePointOptions = value; } }

        /// <summary>
        /// The constructor which creates a new class using blank options for setting manually.
        /// It is expected that this will not be needed as options will always be loaded from the database so this might be deprecated eventually.
        /// </summary>
        public HDCConfigurationOptions()
        {
            //This constructor shouldn't do too many changes as they will be overwritten by the XML deserialiser
            //We'll make sure that we have blank copies of each class though in case someone uses the class in another way
            this.ClearAllSettings();
        }

        /// <summary>
        /// Overwrite all options with blank classes
        /// </summary>
        public void ClearAllSettings()
        {
            //Overwrite options with blank classes
            this._databaseOptions = new HDCConfigurationOptionsDatabase();
            this._fileSystemWatcherOptions = new HDCConfigurationOptionsFileSystemWatcher();
            this._sharePointOptions = new HDCConfigurationOptionsSharePoint();
        }



        /// <summary>
        /// Loads configuration options by specifying a file path. Creating MDCConfiguration class with a file path is preferred.
        /// </summary>
        /// <param name="filePath">The path of the configuration file</param>
        /// <returns>The configuration options from the MDCConfiguration class</returns>
        public static HDCConfigurationOptions LoadFromFile(string filePath)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException("The file path has to exist when specifying LoadFromFile");
            }
            //Create a blank config class
            HDCConfiguration newConfig = new HDCConfiguration(filePath);
            return newConfig.ConfigurationOptions;
        }
    }

    /// <summary>
    /// Class containing configuration data for the file system watcher. The ID for the database record is serialisable to disk as part of the configuration file. All other properties are stored in the database.
    /// </summary>
    public class HDCConfigurationOptionsFileSystemWatcher : HDCConfigurationOptionsBase
    {
        //Constants
        private const string UNCPathDatabaseKey = "UNCPath";
        private const string LocalPathDatabaseKey = "LocalPath";
        private const string WebPathDatabaseKey = "WebPath";
        private const string TrackingFolderDatabaseKey = "TrackingFolderName";
        private const string TrackingFileDatabaseKey = "TrackingFileName";
        private const string DetailsPageRedirFileNameDatabaseKey = "DetailsPageRedirFileName";
        private const string BasePathDepthDatabaseKey = "BasePathDepthInWatchFolder";
        private const string FolderNamesToIgnoreDatabaseKey = "FolderNameToIgnore";
        private const string FileNamesToIgnoreDatabaseKey = "FileNameToIgnore";
        private const string LoggingVerbosityDatabaseKey = "LoggingVerbosity";

        //Fields
        private string _uncPath = "";
        private string _localPath = "";
        private string _webPath = "";
        private string _trackingFolderName = "";
        private string _trackingFileName = "";
        private string _detailsPageRedirFileName = "";
        private int _basePathDepthInWatchFolder;
        private string[] _folderNamesToIgnore;
        private string[] _fileNamesToIgnore;
        private TraceLevel _loggingVerbosity = TraceLevel.Warning;

        //Properties

        /// <summary>
        /// The UNC path for the folder being monitored by the file system watcher
        /// </summary>
        [XmlIgnore()]
        public string UNCPath { get { return this._uncPath; } set { this._uncPath = value; } }

        /// <summary>
        /// The local path for the folder being monitored by the file system watcher
        /// </summary>
        [XmlIgnore()]
        public string LocalPath { get { return this._localPath; } set { this._localPath = value; } }

        /// <summary>
        /// A URL for the folder being monitored by the file system watcher, so files can be downloaded via HTTP
        /// </summary>
        [XmlIgnore()]
        public string WebPath { get { return this._webPath; } set { this._webPath = value; } }

        /// <summary>
        /// The configured name of the folder created in each data set, e.g. '.dataset' or '.experiment'
        /// </summary>
        [XmlIgnore()]
        public string TrackingFolderName { get { return this._trackingFolderName; } set { this._trackingFolderName = value; } }

        /// <summary>
        /// The configured name of the file created inside the tracking folder for finding the data set's metadata
        /// </summary>
        [XmlIgnore()]
        public string TrackingFileName { get { return this._trackingFileName; } set { this._trackingFileName = value; } }

        /// <summary>
        /// The configured name of the HTML file to be created inside the tracking folder. The HTML file will redirect the user to the web page used to populate metadata.
        /// </summary>
        [XmlIgnore()]
        public string DetailsPageRedirFileName { get { return this._detailsPageRedirFileName; } set { this._detailsPageRedirFileName = value; } }

        /// <summary>
        /// The level within the watch folder at which data sets will be recognised.
        /// A depth of 0 means one folder shared with everyone.
        /// A depth of 1 allows one folder per user.
        /// A level of 2 allows one folder per group, with users' folders beneath that.
        /// </summary>
        [XmlIgnore()]
        public int BasePathDepthInWatchFolder { get { return this._basePathDepthInWatchFolder; } set { this._basePathDepthInWatchFolder = value; } }

        /// <summary>
        /// A list of folder names for the file system watcher to ignore, such as the tracking folder's name
        /// </summary>
        [XmlIgnore()]
        public string[] FolderNamesToIgnore { get { return this._folderNamesToIgnore; } set { this._folderNamesToIgnore = value; } }

        /// <summary>
        /// A list of file names for the file system watcher to ignore, such as thumbs.db and .DS_Store
        /// </summary>
        [XmlIgnore()]
        public string[] FileNamesToIgnore { get { return this._fileNamesToIgnore; } set { this._fileNamesToIgnore = value; } }

        /// <summary>
        /// The amount of logging the file system watcher will do (see the TraceLevel enum).
        /// </summary>
        [XmlIgnore()]
        public TraceLevel LoggingVerbosity { get { return this._loggingVerbosity; } set { this._loggingVerbosity = value; } }

        /// <summary>
        /// The ID of the Experiment record in the database where the configuration settings will be saved and loaded from.
        /// This property is serialised to disk in the configuration file when HDCConfigurationOptions is saved.
        /// </summary>
        [XmlAttribute("configurationcontainerid")]
        public int ConfigurationContainerID { get { return base._configurationContainerID; } set { base._configurationContainerID = value; } }

        /// <summary>
        /// Default constructor which sets default options of a data set type of FileStoreOptions and a default name of the machine's name.
        /// </summary>
        public HDCConfigurationOptionsFileSystemWatcher()
        {
            base._containerType = (int)HDCExperimentTypes.FileStoreOptions;
            base._containerName = System.Environment.MachineName;
        }

        /// <summary>
        /// Constructor which sets the data set ID to the passed parameter.
        /// </summary>
        /// <param name="containerid">The ID of the data set being used for storing the configuration.</param>
        public HDCConfigurationOptionsFileSystemWatcher(int containerid)
            : base()
        {
            this._configurationContainerID = containerid;
        }

        /// <summary>
        /// Retrieves the configuration from the database.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the load.</param>
        public override void LoadConfigurationFromDatabase(MaterialsMetadataContext dbcontext)
        {
            Experiment associatedExperiment = base.GetAssociatedExperiment(dbcontext, false);
            if (associatedExperiment != null)
            {
                ExperimentParameter uncPathParam = associatedExperiment.LookupParameter(UNCPathDatabaseKey);
                ExperimentParameter localPathParam = associatedExperiment.LookupParameter(LocalPathDatabaseKey);
                ExperimentParameter webPathParam = associatedExperiment.LookupParameter(WebPathDatabaseKey);
                ExperimentParameter trackingFolderNameParam = associatedExperiment.LookupParameter(TrackingFolderDatabaseKey);
                ExperimentParameter trackingFileNameParam = associatedExperiment.LookupParameter(TrackingFileDatabaseKey);
                ExperimentParameter detailsPageRedirFileNameParam = associatedExperiment.LookupParameter(DetailsPageRedirFileNameDatabaseKey);
                ExperimentParameter basePathDepthParam = associatedExperiment.LookupParameter(BasePathDepthDatabaseKey);
                ExperimentParameter[] folderNamesToIgnoreParams = associatedExperiment.LookupParameters(FolderNamesToIgnoreDatabaseKey);
                ExperimentParameter[] fileNamesToIgnoreParams = associatedExperiment.LookupParameters(FileNamesToIgnoreDatabaseKey);
                ExperimentParameter loggingVerbosityParam = associatedExperiment.LookupParameter(LoggingVerbosityDatabaseKey);

                if (uncPathParam != null) this._uncPath = uncPathParam.Value;
                if (localPathParam != null) this._localPath = localPathParam.Value;
                if (webPathParam != null) this._webPath = webPathParam.Value;
                if (trackingFolderNameParam != null) this._trackingFolderName = trackingFolderNameParam.Value;
                if (trackingFileNameParam != null) this._trackingFileName = trackingFileNameParam.Value;
                if (detailsPageRedirFileNameParam != null) this._detailsPageRedirFileName = detailsPageRedirFileNameParam.Value;
                if (basePathDepthParam != null) int.TryParse(basePathDepthParam.Value, out this._basePathDepthInWatchFolder);
                if (folderNamesToIgnoreParams.Count() > 0) this._folderNamesToIgnore = folderNamesToIgnoreParams.Select(param => param.Value).ToArray();
                if (fileNamesToIgnoreParams.Count() > 0) this._fileNamesToIgnore = fileNamesToIgnoreParams.Select(param => param.Value).ToArray();

                int loggingVerbosityParamValue;
                if (loggingVerbosityParam != null
                    && int.TryParse(loggingVerbosityParam.Value, out loggingVerbosityParamValue)
                    && loggingVerbosityParamValue >= (int)TraceLevel.Off
                    && loggingVerbosityParamValue <= (int)TraceLevel.Verbose
                    )
                {
                    this._loggingVerbosity = (TraceLevel)loggingVerbosityParamValue;
                }
                
            }
        }

        /// <summary>
        /// Writes the current configuration to the database.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the save</param>
        public override void SaveConfigurationToDatabase(MaterialsMetadataContext dbcontext)
        {
            Experiment associatedExperiment = base.GetAssociatedExperiment(dbcontext, true);
            if (associatedExperiment != null)
            {
                associatedExperiment.SaveParameterValue(dbcontext, UNCPathDatabaseKey, this._uncPath, true);
                associatedExperiment.SaveParameterValue(dbcontext, LocalPathDatabaseKey, this._localPath, true);
                associatedExperiment.SaveParameterValue(dbcontext, WebPathDatabaseKey, this._webPath, true);
                associatedExperiment.SaveParameterValue(dbcontext, TrackingFolderDatabaseKey, this._trackingFolderName, true);
                associatedExperiment.SaveParameterValue(dbcontext, TrackingFileDatabaseKey, this._trackingFileName, true);
                associatedExperiment.SaveParameterValue(dbcontext, BasePathDepthDatabaseKey, this._basePathDepthInWatchFolder.ToString(), true);
                associatedExperiment.SaveParameterValue(dbcontext, LoggingVerbosityDatabaseKey, ((int)this._loggingVerbosity).ToString(), true);

                //Delete existing FolderNamesToIgnore parameters
                foreach (ExperimentParameter deleteparam in associatedExperiment.LookupParameters(FolderNamesToIgnoreDatabaseKey))
                {
                    dbcontext.DeleteExperimentParameter(deleteparam.ID, false);
                }

                //Create new FolderNamesToIgnore parameters
                foreach (string ignoreFolder in this._folderNamesToIgnore)
                {
                    associatedExperiment.AddParameter(dbcontext, FolderNamesToIgnoreDatabaseKey, ignoreFolder);
                }

                //Delete existing FileNamesToIgnore parameters
                foreach (ExperimentParameter deleteparam in associatedExperiment.LookupParameters(FileNamesToIgnoreDatabaseKey))
                {
                    dbcontext.DeleteExperimentParameter(deleteparam.ID, false);
                }

                //Create new FileNamesToIgnore parameters
                foreach (string ignoreFile in this._fileNamesToIgnore)
                {
                    associatedExperiment.AddParameter(dbcontext, FileNamesToIgnoreDatabaseKey, ignoreFile);
                }

                dbcontext.SaveChanges();
            }
        }
    }

    /// <summary>
    /// Class containing configuration data for the front end in SharePoint. The ID for the database record is serialisable to disk as part of the configuration file. All other properties are stored in the database.
    /// </summary>
    public class HDCConfigurationOptionsSharePoint : HDCConfigurationOptionsBase
    {
        //Constants
        private const string SiteCollectionURLDatabaseKey = "SiteCollectionURL";

        //Fields
        private string _siteCollectionUrl = "";

        //Properties

        /// <summary>
        /// Full URL of the SharePoint front end for generating URLs
        /// </summary>
        [XmlIgnore()]
        public string SiteCollectionUrl { get { return _siteCollectionUrl; } set { _siteCollectionUrl = value; } }

        /// <summary>
        /// The ID of the Experiment record in the database where the configuration settings will be saved and loaded from.
        /// This property is serialised to disk in the configuration file when HDCConfigurationOptions is saved.
        /// </summary>
        [XmlAttribute("configurationcontainerid")]
        public int ConfigurationContainerID { get { return base._configurationContainerID; } set { base._configurationContainerID = value; } }

        /// <summary>
        /// Default constructor which sets default options of a data set type of SharePointInterfaceOptions and a default name of the machine's name.
        /// </summary>
        public HDCConfigurationOptionsSharePoint()
        {
            base._containerType = (int)HDCExperimentTypes.SharePointInterfaceOptions;
            base._containerName = System.Environment.MachineName;
        }

        /// <summary>
        /// Retrieves the configuration from the database.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the load.</param>
        public override void LoadConfigurationFromDatabase(MaterialsMetadataContext dbcontext)
        {
            Experiment associatedExperiment = base.GetAssociatedExperiment(dbcontext, false);
            //If we don't have a configuration container, then we leave everything set to the default values
            //User can then call SaveConfigurationToDatabase if they want to create new container
            if (associatedExperiment != null)
            {
                ExperimentParameter paramToLoad = null;
                paramToLoad = associatedExperiment.LookupParameter(SiteCollectionURLDatabaseKey);
                if (paramToLoad != null)
                {
                    this._siteCollectionUrl = paramToLoad.Value;
                }
            }
        }

        /// <summary>
        /// Writes the current configuration to the database.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the save</param>
        public override void SaveConfigurationToDatabase(MaterialsMetadataContext dbcontext)
        {
            Experiment associatedExperiment = base.GetAssociatedExperiment(dbcontext, true);
            if (associatedExperiment != null)
            {
                associatedExperiment.SaveParameterValue(dbcontext, SiteCollectionURLDatabaseKey, this._siteCollectionUrl, true);
                dbcontext.SaveChanges();
            }
        }
    }

    /// <summary>
    /// XML serialisable configuration class containing HDC configuration data for accessing the database.
    /// </summary>
    public class HDCConfigurationOptionsDatabase
    {
        //Fields
        private string _databaseServer = "";
        private string _databaseName = "HDCMetadata";

        //Properties

        /// <summary>
        /// The name of the database server
        /// </summary>
        [XmlAttribute("databaseserver")]
        public string DatabaseServer { get { return _databaseServer; } set { _databaseServer = value; } }

        /// <summary>
        /// The name of the database
        /// </summary>
        [XmlAttribute("databasename")]
        public string DatabaseName { get { return _databaseName; } set { _databaseName = value; } }

        /// <summary>
        /// An automatically generated SQL Connection string based on the configuration options
        /// </summary>
        [XmlIgnore()]
        public string SqlConnectionString
        {
            get
            {
                SqlConnectionStringBuilder connectionStringBuilder = new SqlConnectionStringBuilder();
                connectionStringBuilder.DataSource = this._databaseServer;
                connectionStringBuilder.InitialCatalog = this._databaseName;
                connectionStringBuilder.IntegratedSecurity = true;
                connectionStringBuilder.MultipleActiveResultSets = true;
                return connectionStringBuilder.ToString();
            }
        }

        /// <summary>
        /// An automatically generated Entity Framework connection string, based on the configuration options
        /// </summary>
        [XmlIgnore()]
        public string EntityConnectionString
        {
            get
            {
                // Initialize the EntityConnectionStringBuilder.
                EntityConnectionStringBuilder entityBuilder = new EntityConnectionStringBuilder();
                //Set the provider name.
                entityBuilder.Provider = "System.Data.SqlClient";
                // Set the provider-specific connection string.
                entityBuilder.ProviderConnectionString = this.SqlConnectionString;
                // Set the Metadata location.
                entityBuilder.Metadata = @"res://*/MaterialsMetadata.csdl|res://*/MaterialsMetadata.ssdl|res://*/MaterialsMetadata.msl";
                return entityBuilder.ToString();
            }
        }

        /// <summary>
        /// Default constructor
        /// </summary>
        public HDCConfigurationOptionsDatabase()
        {
        }
    }

    /// <summary>
    /// Abstract base class for configuration options which defines abstract methods and shared methods for HDCConfigurationOptions classes
    /// which hold their configuration data in a data set record in the HDC database
    /// </summary>
    public abstract class HDCConfigurationOptionsBase
    {
        #region Fields

        /// <summary>
        /// The ID of the Experiment record in the database where the configuration settings will be saved and loaded from. Defaults to 0 unless overridden by parent class.
        /// </summary>
        protected int _configurationContainerID = 0;
        
        /// <summary>
        /// The type of data set in the database
        /// </summary>
        protected int _containerType = (int)HDCExperimentTypes.None;

        /// <summary>
        /// The name of the data set in the database
        /// </summary>
        protected string _containerName = "None";

        #endregion


        #region Abstract methods

        /// <summary>
        /// Abstract method for defining how the configuration data stored in the class should be saved as metadata in the database
        /// </summary>
        /// <param name="dbcontext">A database context for performing the save of the configuration parameters</param>
        public abstract void SaveConfigurationToDatabase(MaterialsMetadataContext dbcontext);

        /// <summary>
        /// Abstract method for defining how the configuration data should be loaded from the database
        /// </summary>
        /// <param name="dbcontext">A database context for performing the load of the configuration parameters</param>
        public abstract void LoadConfigurationFromDatabase(MaterialsMetadataContext dbcontext);

        #endregion


        #region Non-abstract base methods

        /// <summary>
        /// This returns the database record that holds the saved configuration data. If one is not currently used, one can be optionally created.
        /// </summary>
        /// <param name="dbcontext">A database context to perform the retrieval or creation</param>
        /// <param name="create">true if a new record should be created</param>
        /// <returns>The Experiment containing the metadata used for configuration data or null if one could not be found (and 'create' is set to false)</returns>
        public Experiment GetAssociatedExperiment(MaterialsMetadataContext dbcontext, bool create)
        {
            Experiment matchingExperiment = null;

            //If the container ID is 0 then we don't have a container yet
            if (this._configurationContainerID > 0)
            {
                //Find the matching container, or null if it doesn't exist
                matchingExperiment = dbcontext.Experiments.FirstOrDefault(exp => exp.ID == this._configurationContainerID);
            }
            else
            {
                //Only create new container if we have a container type
                if (create)
                {
                    matchingExperiment = this.CreateNewConfigurationContainer(dbcontext);
                }
                else
                {
                    //.NET generally returns null when lookup fails
                    matchingExperiment = null;
                }
            }

            return matchingExperiment;
        }

        /// <summary>
        /// Creates a new data set in the database for storing configuration data.
        /// The type of the data set is set to _containerType, which should be set in the overridden class to a type that exists in the ExperimentTypes table.
        /// The HDCExperimentTypes enum should be used for ensuring types are set consistently.
        /// _containerType should be negative to indicate it is a special system record and should exist in the ExperimentTypes table.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the operation</param>
        /// <returns>A newly created Experiment, or null if the _containerType is not set correctly</returns>
        public Experiment CreateNewConfigurationContainer(MaterialsMetadataContext dbcontext)
        {
            Experiment newExperiment = null;
            if (this._containerType < 0) //Special containers have negative numbers
            {
                //Create container
                newExperiment = new Experiment();
                newExperiment.Name = this._containerName;
                newExperiment.DefaultBasePath = "None";
                newExperiment.IsDeleted = true;
                newExperiment.Date = DateTime.Now;
                newExperiment.User = dbcontext.Users.First(usr => usr.ID == 0);
                //Set the ExperimentType (entity framework doesn't make this concise!)
                ExperimentType newExpType = dbcontext.ExperimentTypes.FirstOrDefault(type => type.ID == this._containerType);
                if (newExpType != null)
                {
                    newExperiment.ExperimentType = newExpType;
                    //Only add the container if we have a ExperimentType (as it this field can't be null)
                    dbcontext.AddToExperiments(newExperiment);
                    dbcontext.SaveChanges();
                }
            }
            else
            {
                newExperiment = null;
            }

            return newExperiment;
        }

        /// <summary>
        /// Sets an existing database record as the active configuration record.
        /// The record must exist and its ExperimentType must match _containerType.
        /// </summary>
        /// <param name="dbcontext">A database context for performing the operation</param>
        /// <param name="newContainerID">The ID of the record containing the configuration data</param>
        public void AssociateWithExistingConfigurationContainer(MaterialsMetadataContext dbcontext, int newContainerID)
        {
            Experiment newContainer = dbcontext.Experiments.FirstOrDefault(exp => exp.ID == newContainerID);
            //Check we have a container and it has the right type
            if (newContainer != null && newContainer.ExperimentType != null && newContainer.ExperimentType.ID == this._containerType)
            {
                this._configurationContainerID = newContainer.ID;
                this.LoadConfigurationFromDatabase(dbcontext);
            }
            else
            {
                newContainer = null;
            }
        }

        #endregion
    }
}